 <!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>自助默字</title>
<meta name="viewport" content="width=device-width,initial-scale=1,user-scalable=no">
<style>
body{font-family:-apple-system,BlinkMacSystemFont,"Helvetica Neue",Helvetica,"PingFang SC","Hiragino Sans GB","Microsoft YaHei",sans-serif;padding:15px;background:#fafafa;min-height:100vh;display:flex;flex-direction:column}
h1{text-align:center;font-size:26px;margin:10px 0;letter-spacing:1px}

/* 使用说明折叠面板 */
#helpBox{background:#fff;border:1px solid #ddd;border-radius:8px;overflow:hidden;margin-bottom:15px}
#helpTitle{background:linear-gradient(135deg,#667eea 0%,#764ba2 100%);color:#fff;padding:10px 12px;font-size:15px;cursor:pointer;display:flex;justify-content:space-between;align-items:center}
#helpTitle::after{content:'▼';transition:transform .2s}
#helpTitle.open::after{transform:rotate(180deg)}
#helpContent{display:none;padding:12px;font-size:14px;line-height:1.8;color:#333}
#helpContent h4{margin:8px 0 4px}
#helpContent ul{margin:4px 0;padding-left:18px}

.row{margin:12px 0;display:flex;align-items:center}
.row label{width:90px;font-size:15px}
.row input,.row select{flex:1;padding:10px;font-size:15px;border:1px solid #ccc;border-radius:6px}
.btnRow{display:flex;gap:8px}
.btnRow button{flex:1;padding:12px 0;font-size:18px;border:none;border-radius:30px;color:#fff;position:relative;overflow:hidden;transition:transform .1s,box-shadow .2s}
.btnRow button:active{transform:scale(.96)}
.btnRow button::after{content:'';position:absolute;top:50%;left:50%;width:0;height:0;border-radius:50%;background:rgba(255,255,255,.6);transform:translate(-50%,-50%);transition:width .4s,height .4s}
.btnRow button:active::after{width:300px;height:300px}
#btnStart{background:linear-gradient(135deg,#667eea 0%,#764ba2 100%);box-shadow:0 2px 8px rgba(102,126,234,.4)}
#btnPause{background:linear-gradient(135deg,#f093fb 0%,#f5576c 100%);box-shadow:0 2px 8px rgba(245,87,108,.4)}
#btnEnd{background:linear-gradient(135deg,#fa709a 0%,#fee140 100%);box-shadow:0 2px 8px rgba(250,112,154,.4)}
button:disabled{opacity:.5;transform:none!important}

#btnClear{width:100%;margin-top:20px;padding:14px 0;font-size:18px;border:none;border-radius:30px;color:#fff;background:linear-gradient(135deg,#a8edea 0%,#fed6e3 100%);box-shadow:0 2px 8px rgba(168,237,234,.4);position:relative;overflow:hidden;transition:transform .1s,box-shadow .2s}
#btnClear:active{transform:scale(.96)}
#btnClear::after{content:'';position:absolute;top:50%;left:50%;width:0;height:0;border-radius:50%;background:rgba(255,255,255,.6);transform:translate(-50%,-50%);transition:width .4s,height .4s}
#btnClear:active::after{width:300px;height:300px}

#countdown{font-size:60px;text-align:center;margin:20px 0}
#info{text-align:center;font-size:18px;margin:10px 0;user-select:none}
#bar{width:100%;height:8px;background:#eee;border-radius:4px;margin:10px 0;overflow:hidden}
#fill{height:100%;background:linear-gradient(135deg,#667eea 0%,#764ba2 100%);border-radius:4px;width:0%;transition:width .3s}
#again{display:none;width:100%;margin-top:15px;border-radius:30px;background:linear-gradient(135deg,#667eea 0%,#764ba2 100%)}
.watermark{margin-top:auto;text-align:center;font-size:12px;color:#bbb;padding:10px 0}
</style>
</head>
<body>
<h1>自动默写器</h1>
<p style="text-align:center;font-size:14px;color:#666;">✈️本网页必须联网使用✈️</p>

<!-- 使用说明 -->
<div id="helpBox">
  <div id="helpTitle">使用说明 ▼</div>
  <div id="helpContent">
    <h4>① 词语输入</h4>
    <ul><li>用空格隔开每个词语，如：很好 下次 一定  不用担心输入太多会出问题，肯定够用</li></ul>
    <h4>② 参数含义</h4>
    <ul>
      <li>重复：每个词语朗读几次</li>
      <li>单间隔：同一词语两次朗读之间的秒数</li>
      <li>词语间隔：当同一个词语读完后，换下一个词语前的等待秒数</li>
      <li>倒计时：点击“开始”后准备秒数</li>
      <li>乱序：开启后词语顺序随机打乱</li>
    </ul>
    <h4>③ 控制按钮</h4>
    <ul>
      <li>暂停/继续：随时中断或恢复朗读</li>
      <li>结束：立即停止并回到初始状态</li>
      <li>清空词语：一键清空输入框</li>
      <li>切换音色：切换成合适的语言，实现默写英语，日语等，当然你可以用通用角色
    </ul>
    <h4>④ 进度与提示</h4>
    <ul><li>顶部大字显示当前词语及次数；彩色横条实时显示整体进度</li></ul>
    <h4>⑤ 特别感谢</h4>
    <ul><li>特别感谢我的弟弟，因为默字耽误我写代码的时间，想到还有那么多人被困扰，我心如刀割，于是写了这款网页</li>
    <li>特别感谢@WangJinQiang大佬的tts语音系统，真的帮大忙了</li>
        <li>如果哪天发现读不出来了，那就是语音网页挂了，你可以使用谷歌浏览器的本地语音引擎，当然那太机械了。本人会争取做成链接的形式，或者本地搭载一个语音模型做成软件的形式，这样的话就不用担心会挂了也能离线运行了。
    </ul>
  </div>
</div>

<div class="row">
  <label>词语</label>
  <input id="words" placeholder="很好 下次 一定" value="很好 下次 一定">
</div>

<div class="row">
  <label>重复</label>
  <input id="repeat" type="number" min="1" max="10" value="3">
</div>

<div class="row">
  <label>单间隔(秒)</label>
  <input id="gap" type="number" min="0.1" max="60" step="0.1" value="5">
</div>

<div class="row">
  <label>词语间隔(秒)</label>
  <input id="blockGap" type="number" min="0.1" max="300" step="0.1" value="5">
</div>

<div class="row">
  <label>倒计时(秒)</label>
  <input id="cdTime" type="number" min="0.1" max="30" step="0.1" value="0.5">
</div>

<div class="row">
  <label>音色</label>
  <select id="voiceSelect">
    <option value="zh-CN-XiaoxiaoNeural">晓晓 普通话</option>
    <option value="zh-CN-sichuan-YunxiNeural">云兮 四川</option>
    <option value="zh-HK-WanLungNeural">云龙 香港</option>
    <option value="zh-CN-YunxiaNeural">云夏 普通话</option>
    <option value="zh-CN-YunyangNeural">云样 普通话</option>
    <option value="en-GB-EthanNeural">Ethab 英语</option>
    <option value="en-AU-KenNeural">Ken 英语</option>
    <option value="en-US-AvaNeural">Ave 英语</option>
    <option value="ja-JP-DaichiNeural">大志日语</option>
    <option value="zh-CN-XiaochenMultilingualNeural">晓晨通用</option>
  </select>
</div>

<div class="row">
  <label>乱序</label>
  <select id="shuffle">
    <option value="1">开启</option>
    <option value="0">关闭</option>
  </select>
</div>

<div class="btnRow">
  <button id="btnStart">开始</button>
  <button id="btnPause">暂停</button>
  <button id="btnEnd">结束</button>
</div>
<button id="btnClear">清空词语</button>

<div id="countdown"></div>
<div id="info"></div>
<div id="bar"><div id="fill"></div></div>
<button id="again">再来一遍</button>

<div class="watermark">@偿尝 2561025680@qq.com</div>

<script>

/* ====== 折叠面板 ====== */
const helpTitle=document.getElementById('helpTitle');
const helpContent=document.getElementById('helpContent');
helpTitle.onclick=()=>{
  const isOpen=helpContent.style.display==='block';
  helpContent.style.display=isOpen?'none':'block';
  helpTitle.classList.toggle('open',!isOpen);
};

/* ====== 全局变量 ====== */
let audios={};
let list=[],r,g,b,pos=0,cnt=0,cd;
let timer=null,pauseFlag=false;
let totalSteps=0,curStep=0;
let endedFlag=false;

const $=id=>document.getElementById(id);
const voiceSelect = $('voiceSelect');  voiceSelect.onchange = () => { audios = {}; };

/* ====== 工具函数 ====== */
function shuffle(arr){
  for(let i=arr.length-1;i>0;i--){
    const j=Math.floor(Math.random()*(i+1));
    [arr[i],arr[j]]=[arr[j],arr[i]];
  }
  return arr;
}
function buildUrl(t){
  const v=$('voiceSelect').value;
  return 'https://t.leftsite.cn/tts?t='+encodeURIComponent(t)+'&v='+v+'&r=0&p=0';
}
function preload(list){
  list.forEach(w=>{
    if(audios[w]) return;
    audios[w]=new Audio(buildUrl(w));
  });
}
async function sleep(ms){
  return new Promise(resolve=>{timer=setTimeout(resolve,ms);});
}
async function waitEnded(audio){
  return new Promise(resolve=>{
    audio.onended=resolve;
    audio.play();
  });
}
function updateBar(){
  $('fill').style.width=(curStep/totalSteps*100).toFixed(2)+'%';
}
function setPauseUI(paused){
  $('btnPause').textContent=paused?'继续':'暂停';
}

/* ====== 主流程 ====== */
async function start(){
    $('voiceSelect').disabled = true;
  const raw=$('words').value.trim().split(/\s+/);
  if(raw.length<1){alert('请输入词语');return;}
  list=$('shuffle').value==='1'?shuffle([...raw]):[...raw];
  r=parseInt($('repeat').value);
  g=parseFloat($('gap').value)*1000;
  b=parseFloat($('blockGap').value)*1000;
  cd=parseFloat($('cdTime').value);
  endedFlag=false;

  $('btnStart').disabled=true;
  $('again').style.display='none';
  $('btnPause').disabled=false;
  $('btnEnd').disabled=false;
  preload(list);

  totalSteps=list.length*r;
  curStep=0;
  updateBar();

  /* 倒计时 */
  for(let i=cd;i>0;i-=0.1){
    if(endedFlag)return;
    if(pauseFlag)await waitResume();
    $('countdown').textContent=i.toFixed(1);
    await sleep(100);
  }
  $('countdown').textContent='';

  /* 大循环 */
  for(pos=0;pos<list.length;pos++){
    if(endedFlag)return;
    const w=list[pos];
    for(cnt=1;cnt<=r;cnt++){
      if(endedFlag)return;
      if(pauseFlag)await waitResume();
      curStep++;
      updateBar();
      $('info').textContent=`${w}  (${cnt}/${r})`;
      await waitEnded(audios[w]);
      if(cnt<r&&!endedFlag)await sleep(g);
    }
    if(pos<list.length-1&&!endedFlag)await sleep(b);
  }

  if(!endedFlag){
    $('info').textContent='完成！';
    $('fill').style.width='100%';
    $('again').style.display='block';
  }
  resetAfterEnd();
}

function resetAfterEnd(){
  $('btnStart').disabled=false;
  $('btnPause').disabled=true;
  $('btnEnd').disabled=true;
  $('voiceSelect').disabled = false;
  setPauseUI(false);
}

function waitResume(){
  return new Promise(res=>{
    const t=setInterval(()=>{
      if(!pauseFlag||endedFlag){clearInterval(t);res();}
    },300);
  });
}

/* ====== 事件绑定 ====== */
$('btnStart').onclick=()=>{pauseFlag=false; start();};

$('btnPause').onclick=()=>{
  pauseFlag=!pauseFlag;
  setPauseUI(pauseFlag);
};

$('btnEnd').onclick=()=>{
  endedFlag=true;
  clearTimeout(timer);
  if(audios[list[pos]]) audios[list[pos]].pause();
  $('info').textContent='已结束';
  $('countdown').textContent='';
  resetAfterEnd();
};

$('btnClear').onclick=()=>{
  $('words').value='';
  $('words').focus();
};

$('again').onclick=()=>{pos=0;cnt=0; start();};

/* 初始状态 */
$('btnPause').disabled=true;
$('btnEnd').disabled=true;
</script>
</body>
</html>
